--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit ebcd3c6acd19cbc0473fb51556e2aef4dddb55e0
Parents : 0f50346
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-04-16T23:32:54-05:00
feat(electron): enhance backend connection handling and UI updates
Changes
11 files changed, 454 insertions(+), 90 deletions(-)
Diff
diff --git a/electron/crash.html b/electron/crash.html
index 159c9cb8..277427d8 100644
--- a/electron/crash.html
+++ b/electron/crash.html
@@ -11,32 +11,23 @@
<script src="./assets/js/tailwindcss/tailwind-v3.4.3-forms-v0.5.7.js"></script>
</head>
<body
- class="min-h-screen bg-slate-100 text-gray-900 antialiased dark:bg-zinc-950 dark:text-zinc-50 transition-colors"
+ class="min-h-screen antialiased bg-gradient-to-b from-slate-100 to-slate-200 text-slate-800 dark:from-zinc-950 dark:to-zinc-900 dark:text-zinc-100 transition-colors"
>
- <div class="absolute inset-0 -z-10 overflow-hidden">
- <div
- class="absolute -left-32 -top-40 h-80 w-80 rounded-full bg-gradient-to-br from-red-500/30 via-orange-500/20 to-rose-500/30 blur-3xl dark:from-red-600/25 dark:via-orange-600/25 dark:to-rose-600/25"
- ></div>
- <div
- class="absolute -right-24 top-20 h-64 w-64 rounded-full bg-gradient-to-br from-orange-400/30 via-red-500/20 to-rose-500/30 blur-3xl dark:from-orange-500/25 dark:via-red-500/25 dark:to-rose-500/25"
- ></div>
- </div>
-
- <main class="relative flex min-h-screen items-center justify-center px-4 py-6 sm:px-6">
- <div class="w-full max-w-5xl">
+ <main class="flex min-h-screen items-center justify-center px-4 py-8 sm:px-6">
+ <div class="w-full max-w-4xl">
<div
- class="rounded-2xl border border-slate-200/80 bg-white/80 shadow-2xl backdrop-blur-xl ring-1 ring-white/60 dark:border-zinc-800/70 dark:bg-zinc-900/70 dark:ring-zinc-800/70 transition-colors overflow-hidden"
+ class="overflow-hidden rounded-2xl border border-slate-200/80 bg-white/90 shadow-lg shadow-slate-200/50 backdrop-blur-sm dark:border-zinc-700/80 dark:bg-zinc-900/90 dark:shadow-black/40"
>
- <div class="p-4 sm:p-6 space-y-4">
+ <div class="p-5 sm:p-6 space-y-5">
<div
class="flex flex-col sm:flex-row items-center sm:items-start gap-3 text-center sm:text-left"
>
<div
- class="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-red-500 via-orange-500 to-rose-500 shadow-lg ring-4 ring-white/60 dark:ring-zinc-800/70"
+ class="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-red-100 ring-1 ring-red-200 dark:bg-red-950/40 dark:ring-red-900/60"
>
<svg
xmlns="http://www.w3.org/2000/svg"
- class="h-8 w-8 text-white"
+ class="h-7 w-7 text-red-600 dark:text-red-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@@ -50,18 +41,18 @@
</svg>
</div>
<div class="space-y-0.5">
- <div class="text-xl font-semibold tracking-tight text-gray-900 dark:text-white">
+ <div class="text-xl font-semibold tracking-tight text-slate-900 dark:text-white">
MeshChatX Crashed
</div>
- <div class="text-xs text-gray-600 dark:text-gray-300">
- Critical error detected in backend service.
+ <div class="text-xs text-slate-600 dark:text-zinc-400">
+ The backend process exited unexpectedly.
</div>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<div
- class="rounded-xl border border-red-200/90 bg-red-50/70 p-3 dark:border-red-900/40 dark:bg-red-900/20 transition-colors"
+ class="rounded-xl border border-red-200/80 bg-red-50/70 p-3 dark:border-red-900/50 dark:bg-red-950/30 transition-colors"
>
<div
class="text-[10px] uppercase tracking-wide text-red-600 dark:text-red-400 font-semibold"
@@ -76,50 +67,52 @@
</div>
</div>
<div
- class="rounded-xl border border-slate-200/90 bg-white/70 p-3 text-center sm:text-right dark:border-zinc-800/80 dark:bg-zinc-900/70 transition-colors"
+ class="rounded-xl border border-slate-200/80 bg-slate-50/70 p-3 text-center sm:text-right dark:border-zinc-700/70 dark:bg-zinc-950/40 transition-colors"
>
- <div class="text-[10px] uppercase tracking-wide text-gray-500 dark:text-gray-400">
+ <div class="text-[10px] uppercase tracking-wide text-slate-500 dark:text-zinc-400">
Status
</div>
- <div class="mt-0.5 text-base font-semibold text-red-600 dark:text-red-400">Offline</div>
+ <div class="mt-0.5 text-base font-semibold text-red-600 dark:text-red-400">
+ Backend unavailable
+ </div>
</div>
</div>
<div class="space-y-3">
<div class="flex flex-col sm:flex-row items-center justify-between gap-2 px-1">
<h3
- class="text-[10px] font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
+ class="text-[10px] font-semibold uppercase tracking-wider text-slate-500 dark:text-zinc-400"
>
Diagnostic Logs
</h3>
<button
- onclick="copyLogs()"
- class="w-full sm:w-auto text-[10px] font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300 bg-blue-50 dark:bg-blue-900/30 px-3 py-1 rounded-lg transition-colors"
+ onclick="copyLogs(event)"
+ class="w-full sm:w-auto text-[10px] font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300 bg-blue-50 dark:bg-blue-950/40 px-3 py-1 rounded-lg transition-colors"
>
Copy all logs
</button>
</div>
<div class="space-y-1">
- <div class="text-[10px] font-medium text-gray-500 dark:text-gray-400 px-1">
+ <div class="text-[10px] font-medium text-slate-500 dark:text-zinc-400 px-1">
Standard Output (stdout)
</div>
<div class="relative group">
<pre
id="stdout"
- class="h-52 overflow-auto rounded-xl border border-slate-200 bg-slate-50 p-3 font-mono text-[10px] text-slate-700 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-300 select-text scrollbar-thin scrollbar-thumb-slate-300 dark:scrollbar-thumb-zinc-800"
+ class="h-52 overflow-auto rounded-xl border border-slate-200 bg-slate-50 p-3 font-mono text-[10px] text-slate-700 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-300 select-text"
></pre>
</div>
</div>
<div class="space-y-1">
- <div class="text-[10px] font-medium text-gray-500 dark:text-gray-400 px-1">
+ <div class="text-[10px] font-medium text-slate-500 dark:text-zinc-400 px-1">
Standard Error (stderr)
</div>
<div class="relative group">
<pre
id="stderr"
- class="h-64 overflow-auto rounded-xl border border-red-100 bg-red-50/50 p-3 font-mono text-[10px] text-red-700 dark:border-red-900/20 dark:bg-zinc-950 dark:text-red-400 select-text scrollbar-thin scrollbar-thumb-red-200 dark:scrollbar-thumb-zinc-800"
+ class="h-64 overflow-auto rounded-xl border border-red-200/70 bg-red-50/60 p-3 font-mono text-[10px] text-red-700 dark:border-red-900/40 dark:bg-zinc-950 dark:text-red-400 select-text"
></pre>
</div>
</div>
@@ -128,19 +121,13 @@
<div class="flex flex-wrap items-center justify-center sm:justify-start gap-2 pt-2">
<button
onclick="window.electron.relaunch()"
- class="w-full sm:w-40 rounded-xl bg-blue-600 px-4 py-2.5 text-xs font-semibold text-white shadow-lg shadow-blue-500/25 hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-900 transition-all active:scale-[0.98]"
+ class="w-full sm:w-40 rounded-xl bg-blue-600 px-4 py-2.5 text-xs font-semibold text-white shadow-sm hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-900 transition-colors"
>
Relaunch
</button>
- <button
- onclick="window.electron.relaunchEmergency()"
- class="w-full sm:w-48 rounded-xl bg-orange-600 px-4 py-2.5 text-xs font-semibold text-white shadow-lg shadow-orange-500/25 hover:bg-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-900 transition-all active:scale-[0.98]"
- >
- Engage Emergency Mode
- </button>
<button
onclick="window.electron.shutdown()"
- class="w-full sm:w-24 rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-xs font-semibold text-gray-700 shadow-sm hover:bg-slate-50 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800 transition-all active:scale-[0.98]"
+ class="w-full sm:w-24 rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-xs font-semibold text-slate-700 shadow-sm hover:bg-slate-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800 transition-colors"
>
Exit
</button>
@@ -169,7 +156,7 @@
document.getElementById("stderr").innerText = "Error decoding logs.";
}
- function copyLogs() {
+ function copyLogs(event) {
const stdout = document.getElementById("stdout").innerText;
const stderr = document.getElementById("stderr").innerText;
const exitCode = document.getElementById("exit-code").innerText;
@@ -177,7 +164,10 @@
const fullReport = `MeshChatX Crash Report\nExit Code: ${exitCode}\n\n--- STDOUT ---\n${stdout}\n\n--- STDERR ---\n${stderr}`;
navigator.clipboard.writeText(fullReport).then(() => {
- const btn = event.target;
+ const btn = event && event.currentTarget ? event.currentTarget : null;
+ if (!btn) {
+ return;
+ }
const originalText = btn.innerText;
btn.innerText = "Copied!";
btn.classList.replace("text-blue-600", "text-emerald-600");
diff --git a/electron/loading.html b/electron/loading.html
index 5568abdf..09850ae6 100644
--- a/electron/loading.html
+++ b/electron/loading.html
@@ -55,6 +55,10 @@
id="attempt-hint"
class="mt-3 min-h-[1.25rem] text-center text-xs text-slate-500 dark:text-zinc-500"
></p>
+ <p
+ id="connection-notice"
+ class="mt-2 min-h-[2rem] text-center text-xs leading-relaxed text-amber-700 dark:text-amber-300"
+ ></p>
<p class="mt-4 text-center text-[11px] text-slate-400 dark:text-zinc-600" id="app-version">
v0.0.0
</p>
@@ -63,13 +67,20 @@
</div>
</main>
+ <script src="./loadingStatusNotice.js"></script>
<script>
const statusLine = document.getElementById("status-line");
const attemptHint = document.getElementById("attempt-hint");
+ const connectionNotice = document.getElementById("connection-notice");
const API_HOST = "127.0.0.1";
const API_PORT = "9337";
const base = (protocol) => `${protocol}://${API_HOST}:${API_PORT}`;
+ const startupParams = new URLSearchParams(window.location.search);
+ const NOTICE_AFTER_ATTEMPTS = 10;
+ const NETWORK_WARNING_AFTER_ATTEMPTS = 24;
+ const RUNTIME_RECHECK_INTERVAL = 4;
+ const MAX_FAILURE_HISTORY = 8;
let protocolOrder = ["https", "http"];
@@ -78,6 +89,7 @@
listenForSystemThemeChanges();
(async function bootstrap() {
+ applyStartupErrorHint();
try {
if (window.electron && typeof window.electron.backendHttpOnly === "function") {
const httpOnly = await window.electron.backendHttpOnly();
@@ -89,6 +101,16 @@
check();
})();
+ function applyStartupErrorHint() {
+ const startupError = startupParams.get("startup_error");
+ if (startupError !== "backend_unreachable") {
+ return;
+ }
+ statusLine.textContent = "Lost connection to local backend.";
+ connectionNotice.textContent =
+ "Still retrying. If this continues, firewall or localhost filtering may be blocking access.";
+ }
+
async function showAppVersion() {
try {
const appVersion = await window.electron.appVersion();
@@ -127,6 +149,9 @@
let detectedProtocol = "http";
let attemptCount = 0;
+ let runtimeProbeAttempt = 0;
+ let cachedRuntimeState = null;
+ const recentFailures = [];
function parseStatusJson(text) {
try {
@@ -136,44 +161,120 @@
}
}
+ function rememberFailure(failure) {
+ if (!failure || typeof failure !== "object") {
+ return;
+ }
+ recentFailures.push(failure);
+ if (recentFailures.length > MAX_FAILURE_HISTORY) {
+ recentFailures.shift();
+ }
+ }
+
+ async function refreshBackendRuntimeStateIfNeeded() {
+ if (!window.electron || typeof window.electron.backendRuntimeState !== "function") {
+ return;
+ }
+ if (attemptCount - runtimeProbeAttempt < RUNTIME_RECHECK_INTERVAL && cachedRuntimeState) {
+ return;
+ }
+ runtimeProbeAttempt = attemptCount;
+ try {
+ cachedRuntimeState = await window.electron.backendRuntimeState();
+ } catch (e) {}
+ }
+
+ function resolveFetchFailureKind(error) {
+ const helper = window.MeshchatLoadingStatusNotice;
+ if (helper && typeof helper.classifyFetchError === "function") {
+ return helper.classifyFetchError(error);
+ }
+ return "network-error";
+ }
+
async function tryOnce(protocol) {
const url = `${base(protocol)}/api/v1/status`;
- const result = await fetch(url, { cache: "no-store" });
- const text = await result.text();
- if (result.status !== 200) {
- return null;
+ try {
+ const result = await fetch(url, { cache: "no-store" });
+ const text = await result.text();
+ if (result.status !== 200) {
+ return {
+ ok: false,
+ failure: { kind: "http-error", status: result.status, protocol: protocol },
+ };
+ }
+ const data = parseStatusJson(text);
+ if (data && data.status === "ok") {
+ return { ok: true, protocol: protocol };
+ }
+ return {
+ ok: false,
+ failure: { kind: "invalid-payload", protocol: protocol },
+ };
+ } catch (error) {
+ return {
+ ok: false,
+ failure: {
+ kind: resolveFetchFailureKind(error),
+ protocol: protocol,
+ message: String((error && error.message) || ""),
+ },
+ };
+ }
+ }
+
+ function classifyConnectionIssue() {
+ const helper = window.MeshchatLoadingStatusNotice;
+ if (helper && typeof helper.classifyConnectionIssue === "function") {
+ return helper.classifyConnectionIssue(recentFailures, cachedRuntimeState, {
+ attemptCount: attemptCount,
+ networkWarnAfterAttempts: NETWORK_WARNING_AFTER_ATTEMPTS,
+ });
}
- const data = parseStatusJson(text);
- if (data && data.status === "ok") {
- return protocol;
+ return {
+ reason: "starting",
+ headline: "Waiting for backend startup.",
+ detail: "MeshChatX is still initializing services.",
+ };
+ }
+
+ async function updateStartupNotice() {
+ if (attemptCount < NOTICE_AFTER_ATTEMPTS) {
+ connectionNotice.textContent = "";
+ return;
}
- return null;
+ await refreshBackendRuntimeStateIfNeeded();
+ const issue = classifyConnectionIssue();
+ statusLine.textContent = issue.headline;
+ connectionNotice.textContent = issue.detail;
}
async function check() {
attemptCount += 1;
if (attemptCount === 1) {
attemptHint.textContent = "";
+ connectionNotice.textContent = "";
} else {
attemptHint.textContent = "Still starting…";
}
// Prefer HTTPS unless the backend was started with --no-https (then HTTP first).
for (const protocol of protocolOrder) {
- try {
- const ok = await tryOnce(protocol);
- if (ok) {
- detectedProtocol = ok;
- statusLine.textContent = "Opening the app…";
- attemptHint.textContent = "";
- syncThemeFromConfig();
- setTimeout(onReady, 200);
- return;
- }
- } catch (e) {
- continue;
+ const result = await tryOnce(protocol);
+ if (result.ok) {
+ detectedProtocol = result.protocol;
+ statusLine.textContent = "Opening the app…";
+ attemptHint.textContent = "";
+ connectionNotice.textContent = "";
+ syncThemeFromConfig();
+ setTimeout(onReady, 200);
+ return;
+ }
+ if (result.failure) {
+ rememberFailure(result.failure);
}
}
+ await updateStartupNotice();
setTimeout(check, 350);
}
diff --git a/electron/loadingStatusNotice.js b/electron/loadingStatusNotice.js
new file mode 100644
index 00000000..2463b7d0
--- /dev/null
+++ b/electron/loadingStatusNotice.js
@@ -0,0 +1,98 @@
+(function (root, factory) {
+ const exported = factory();
+ if (typeof module !== "undefined" && module.exports) {
+ module.exports = exported;
+ }
+ root.MeshchatLoadingStatusNotice = exported;
+})(typeof globalThis !== "undefined" ? globalThis : window, function () {
+ function toLowerText(value) {
+ if (value == null) {
+ return "";
+ }
+ return String(value).toLowerCase();
+ }
+
+ function classifyConnectionIssue(failures, runtimeState, options) {
+ const entries = Array.isArray(failures) ? failures : [];
+ const state = runtimeState && typeof runtimeState === "object" ? runtimeState : null;
+ const opts = options && typeof options === "object" ? options : {};
+ const attemptCount = Number(opts.attemptCount) || 0;
+ const networkWarnAfterAttempts = Number(opts.networkWarnAfterAttempts) || 24;
+
+ if (state && state.running === false && state.lastExitCode != null) {
+ return {
+ reason: "backend-exited",
+ headline: "The backend process stopped unexpectedly.",
+ detail: "Please restart MeshChatX. If this keeps happening, review crash logs.",
+ };
+ }
+
+ const hasAddressUnreachable = entries.some((entry) => entry && entry.kind === "address-unreachable");
+ if (hasAddressUnreachable) {
+ return {
+ reason: "loopback-blocked",
+ headline: "Cannot reach local backend on 127.0.0.1:9337.",
+ detail: "A firewall, VPN, sandbox, or loopback policy may be blocking local connections.",
+ };
+ }
+
+ const hasNetworkError = entries.some((entry) => entry && entry.kind === "network-error");
+ if (hasNetworkError) {
+ if (attemptCount < networkWarnAfterAttempts) {
+ return {
+ reason: "starting",
+ headline: "Waiting for backend startup.",
+ detail: "MeshChatX is still initializing services.",
+ };
+ }
+ return {
+ reason: "network-blocked",
+ headline: "Still waiting for local backend connection.",
+ detail: "If startup stays stuck, firewall or network filtering software may be blocking localhost traffic.",
+ };
+ }
+
+ const hasServerError = entries.some(
+ (entry) => entry && entry.kind === "http-error" && Number(entry.status) >= 500
+ );
+ if (hasServerError) {
+ return {
+ reason: "backend-http-error",
+ headline: "Backend is running but reported an internal error.",
+ detail: "MeshChatX will keep retrying while the backend finishes startup.",
+ };
+ }
+
+ const hasInvalidPayload = entries.some((entry) => entry && entry.kind === "invalid-payload");
+ if (hasInvalidPayload) {
+ return {
+ reason: "backend-invalid-response",
+ headline: "Backend responded with invalid startup data.",
+ detail: "MeshChatX will continue retrying while the backend stabilizes.",
+ };
+ }
+
+ return {
+ reason: "starting",
+ headline: "Waiting for backend startup.",
+ detail: "MeshChatX is still initializing services.",
+ };
+ }
+
+ function classifyFetchError(error) {
+ const message = `${toLowerText(error && error.name)} ${toLowerText(error && error.message)}`.trim();
+ if (
+ message.includes("err_address_unreachable") ||
+ message.includes("address_unreachable") ||
+ message.includes("ehostunreach")
+ ) {
+ return "address-unreachable";
+ }
+ return "network-error";
+ }
+
+ return {
+ classifyConnectionIssue: classifyConnectionIssue,
+ classifyFetchError: classifyFetchError,
+ };
+});
diff --git a/electron/main.js b/electron/main.js
index 156da78a..50d31a01 100644
--- a/electron/main.js
+++ b/electron/main.js
@@ -44,6 +44,14 @@ var isQuiting = false;
// remember child process for exe so we can kill it when app exits
var exeChildProcess = null;
+var backendRuntimeState = {
+ started: false,
+ running: false,
+ pid: null,
+ lastExitCode: null,
+ lastError: "",
+ lastEventAt: null,
+};
// store integrity status
var integrityStatus = {
@@ -218,6 +226,18 @@ ipcMain.handle("backend-http-only", () => {
return getUserProvidedArguments().includes("--no-https");
});
+ipcMain.handle("backend-runtime-state", () => {
+ const isRunning =
+ !!exeChildProcess &&
+ exeChildProcess.exitCode === null &&
+ exeChildProcess.signalCode === null &&
+ backendRuntimeState.started;
+ return {
+ ...backendRuntimeState,
+ running: isRunning,
+ };
+});
+
// add support for showing an alert window via ipc
ipcMain.handle("alert", async (event, message) => {
return await dialog.showMessageBox(mainWindow, {
@@ -259,13 +279,25 @@ ipcMain.handle("prompt", async (event, message) => {
// allow relaunching app via ipc
ipcMain.handle("relaunch", () => {
- app.relaunch();
- app.exit();
+ const relaunchOptions = {};
+ if (!process.defaultApp && process.platform === "linux" && process.env.APPIMAGE) {
+ relaunchOptions.execPath = process.env.APPIMAGE;
+ }
+ app.relaunch(relaunchOptions);
+ isQuiting = true;
+ quit();
});
ipcMain.handle("relaunch-emergency", () => {
- app.relaunch({ args: process.argv.slice(1).concat(["--emergency"]) });
- app.exit();
+ const relaunchOptions = {
+ args: process.argv.slice(1).concat(["--emergency"]),
+ };
+ if (!process.defaultApp && process.platform === "linux" && process.env.APPIMAGE) {
+ relaunchOptions.execPath = process.env.APPIMAGE;
+ }
+ app.relaunch(relaunchOptions);
+ isQuiting = true;
+ quit();
});
ipcMain.handle("shutdown", () => {
@@ -451,6 +483,18 @@ function getAppIconPath() {
return fs.existsSync(iconPath) ? iconPath : fallbackIconPath;
}
+function isLocalBackendUrl(url) {
+ if (!url || typeof url !== "string") {
+ return false;
+ }
+ return (
+ url.startsWith("http://127.0.0.1:9337") ||
+ url.startsWith("https://127.0.0.1:9337") ||
+ url.startsWith("http://localhost:9337") ||
+ url.startsWith("https://localhost:9337")
+ );
+}
+
function createTray() {
tray = new Tray(getAppIconPath());
const contextMenu = Menu.buildFromTemplate([
@@ -554,6 +598,29 @@ app.whenReady().then(async () => {
mainWindow.webContents.on("unresponsive", () => {
log("Renderer process became unresponsive.");
});
+ mainWindow.webContents.on(
+ "did-fail-load",
+ async (_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
+ if (!isMainFrame || !isLocalBackendUrl(validatedURL)) {
+ return;
+ }
+ log(`Failed to load backend URL (${errorCode}): ${errorDescription} - ${validatedURL}`);
+ if (!mainWindow || mainWindow.isDestroyed()) {
+ return;
+ }
+ const currentUrl = mainWindow.webContents.getURL();
+ if (currentUrl.includes("loading.html")) {
+ return;
+ }
+ try {
+ await mainWindow.loadFile(path.join(__dirname, "loading.html"), {
+ query: { startup_error: "backend_unreachable" },
+ });
+ } catch (error) {
+ log(`Failed to restore loading screen after backend load failure: ${error.message}`);
+ }
+ }
+ );
// minimize to tray behavior
mainWindow.on("close", (event) => {
@@ -704,6 +771,14 @@ app.whenReady().then(async () => {
if (!exeChildProcess || !exeChildProcess.pid) {
throw new Error("Failed to start backend process (no PID).");
}
+ backendRuntimeState = {
+ started: true,
+ running: true,
+ pid: exeChildProcess.pid,
+ lastExitCode: null,
+ lastError: "",
+ lastEventAt: Date.now(),
+ };
// log stdout
var stdoutLines = [];
@@ -736,10 +811,15 @@ app.whenReady().then(async () => {
// log errors
exeChildProcess.on("error", function (error) {
log(error);
+ backendRuntimeState.lastError = error && error.message ? error.message : String(error);
+ backendRuntimeState.lastEventAt = Date.now();
});
// quit electron app if exe dies
exeChildProcess.on("exit", async function (code) {
+ backendRuntimeState.running = false;
+ backendRuntimeState.lastExitCode = code;
+ backendRuntimeState.lastEventAt = Date.now();
// if no exit code provided, we wanted exit to happen, so do nothing
if (code == null) {
return;
@@ -790,6 +870,7 @@ function quit() {
return;
}
if (exeChildProcess.exitCode !== null || exeChildProcess.signalCode !== null) {
+ app.quit();
return;
}
try {
diff --git a/electron/preload.js b/electron/preload.js
index 19dc2e28..cb4315c8 100644
--- a/electron/preload.js
+++ b/electron/preload.js
@@ -96,4 +96,7 @@ contextBridge.exposeInMainWorld("electron", {
backendHttpOnly: async function () {
return await ipcRenderer.invoke("backend-http-only");
},
+ backendRuntimeState: async function () {
+ return await ipcRenderer.invoke("backend-runtime-state");
+ },
});
diff --git a/meshchatx/src/backend/data/THIRD_PARTY_NOTICES.txt b/meshchatx/src/backend/data/THIRD_PARTY_NOTICES.txt
index d08a099a..49b50acd 100644
--- a/meshchatx/src/backend/data/THIRD_PARTY_NOTICES.txt
+++ b/meshchatx/src/backend/data/THIRD_PARTY_NOTICES.txt
@@ -1,27 +1,90 @@
Reticulum MeshChatX - Third-party notices
-Generated at: 2026-04-16T23:23:28.244334Z
+Generated at: 2026-04-17T03:19:30.687503Z
Frontend source: node_modules
Python dependencies
-------------------
+aiohappyeyeballs 2.6.1
+ License: PSF-2.0
+ Author: J. Nick Koston
+aiohttp 3.13.5
+ License: Apache-2.0 AND MIT
+ Author: —
+aiohttp-session 2.12.1
+ License: Apache 2
+ Author: Andrew Svetlov
+aiosignal 1.4.0
+ License: Apache 2.0
+ Author: aiohttp team <team@aiohttp.org>
+attrs 26.1.0
+ License: MIT
+ Author: Hynek Schlawack <hs@ox.cx>
+audioop-lts 0.2.2
+ License: PSF-2.0
+ Author: —
+bcrypt 5.0.0
+ License: Apache-2.0
+ Author: The Python Cryptographic Authority developers <cryptography-dev@python.org>
cffi 2.0.0
License: MIT
Author: Armin Rigo, Maciej Fijalkowski
cryptography 46.0.7
License: Apache-2.0 OR BSD-3-Clause
Author: The Python Cryptographic Authority and individual contributors <cryptography-dev@python.org>
-jaraco.context 6.1.3.dev0+g098f39c91.d20260403
+frozenlist 1.8.0
+ License: Apache-2.0
+ Author: aiohttp team <team@aiohttp.org>
+idna 3.11
+ License: BSD-3-Clause
+ Author: Kim Davies <kim+pypi@gumleaf.org>
+jaraco.context 6.1.2
License: MIT
Author: "Jason R. Coombs" <jaraco@jaraco.com>
+lxmf 0.9.4
+ License: Reticulum License
+ Author: Mark Qvist
+lxmfy 1.6.2
+ License: BSD-0-Clause
+ Author: Quad4
+lxst 0.4.6
+ License: Other/Proprietary License
+ Author: Mark Qvist
+multidict 6.7.1
+ License: Apache License 2.0
+ Author: Andrew Svetlov
+numpy 2.4.4
+ License: BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0
+ Author: Travis E. Oliphant et al.
+ply 3.11
+ License: BSD
+ Author: David Beazley
+propcache 0.4.1
+ License: Apache-2.0
+ Author: Andrew Svetlov
psutil 7.2.2
License: BSD-3-Clause
Author: Giampaolo Rodola
+pycodec2 4.1.1
+ License: OSI Approved :: BSD License
+ Author: Grzegorz Milka
pycparser 3.0
License: BSD-3-Clause
Author: Eli Bendersky <eliben@gmail.com>
+pyserial 3.5
+ License: BSD
+ Author: Chris Liechti
reticulum-meshchatx 4.5.0
License: MIT
Author: Sudo-Ivan
+rns 1.1.5
+ License: Reticulum License
+ Author: Mark Qvist
+websockets 16.0
+ License: BSD-3-Clause
+ Author: Aymeric Augustin <aymeric.augustin@m4x.org>
+yarl 1.23.0
+ License: Apache-2.0
+ Author: Andrew Svetlov
Node dependencies
-----------------
@@ -868,10 +931,10 @@ ci-info 4.4.0
clean-stack 2.2.0
License: MIT
Author: Sindre Sorhus <sindresorhus@gmail.com>
-cli-cursor 4.0.0
+cli-cursor 3.1.0
License: MIT
Author: Sindre Sorhus <sindresorhus@gmail.com>
-cli-cursor 3.1.0
+cli-cursor 4.0.0
License: MIT
Author: Sindre Sorhus <sindresorhus@gmail.com>
cli-spinners 2.9.2
@@ -2200,10 +2263,10 @@ resolve-protobuf-schema 2.1.0
responselike 2.0.1
License: MIT
Author: lukechilds
-restore-cursor 3.1.0
+restore-cursor 4.0.0
License: MIT
Author: Sindre Sorhus <sindresorhus@gmail.com>
-restore-cursor 4.0.0
+restore-cursor 3.1.0
License: MIT
Author: Sindre Sorhus <sindresorhus@gmail.com>
retry 0.12.0
@@ -2485,10 +2548,10 @@ type-check 0.4.0
type-fest 0.21.3
License: (MIT OR CC0-1.0)
Author: Sindre Sorhus <sindresorhus@gmail.com>
-type-fest 0.13.1
+type-fest 1.4.0
License: (MIT OR CC0-1.0)
Author: Sindre Sorhus <sindresorhus@gmail.com>
-type-fest 1.4.0
+type-fest 0.13.1
License: (MIT OR CC0-1.0)
Author: Sindre Sorhus <sindresorhus@gmail.com>
typescript 5.4.5
diff --git a/meshchatx/src/backend/data/licenses_frontend.json b/meshchatx/src/backend/data/licenses_frontend.json
index 22d30b8e..13d2c013 100644
--- a/meshchatx/src/backend/data/licenses_frontend.json
+++ b/meshchatx/src/backend/data/licenses_frontend.json
@@ -1687,13 +1687,13 @@
},
{
"name": "cli-cursor",
- "version": "4.0.0",
+ "version": "3.1.0",
"author": "Sindre Sorhus <sindresorhus@gmail.com>",
"license": "MIT"
},
{
"name": "cli-cursor",
- "version": "3.1.0",
+ "version": "4.0.0",
"author": "Sindre Sorhus <sindresorhus@gmail.com>",
"license": "MIT"
},
@@ -4351,13 +4351,13 @@
},
{
"name": "restore-cursor",
- "version": "3.1.0",
+ "version": "4.0.0",
"author": "Sindre Sorhus <sindresorhus@gmail.com>",
"license": "MIT"
},
{
"name": "restore-cursor",
- "version": "4.0.0",
+ "version": "3.1.0",
"author": "Sindre Sorhus <sindresorhus@gmail.com>",
"license": "MIT"
},
@@ -4921,13 +4921,13 @@
},
{
"name": "type-fest",
- "version": "0.13.1",
+ "version": "1.4.0",
"author": "Sindre Sorhus <sindresorhus@gmail.com>",
"license": "(MIT OR CC0-1.0)"
},
{
"name": "type-fest",
- "version": "1.4.0",
+ "version": "0.13.1",
"author": "Sindre Sorhus <sindresorhus@gmail.com>",
"license": "(MIT OR CC0-1.0)"
},
diff --git a/meshchatx/src/frontend/js/GlobalState.js b/meshchatx/src/frontend/js/GlobalState.js
index 3d1d1838..c1f66a83 100644
--- a/meshchatx/src/frontend/js/GlobalState.js
+++ b/meshchatx/src/frontend/js/GlobalState.js
@@ -27,6 +27,8 @@ const globalState = reactive({
ui_transparency: 0,
ui_glass_enabled: true,
message_list_virtualization: true,
+ warn_on_stranger_links: true,
+ messages_sidebar_position: "left",
},
});
diff --git a/package.json b/package.json
index 782defa6..bbaa6f8d 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,7 @@
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:install": "playwright install chromium",
- "electron-postinstall": "electron-builder install-app-deps",
+ "electron-postinstall": "electron-builder install-app-deps && node scripts/ensure-micron-parser-package.js",
"electron": "pnpm run electron-postinstall && pnpm run build && electron .",
"dist": "pnpm run electron-postinstall && pnpm run build && electron-builder --publish=never",
"dist:linux": "pnpm run electron-postinstall && cross-env PLATFORM=linux pnpm run build && electron-builder --linux AppImage deb --publish=never",
@@ -85,6 +85,7 @@
"jsdom": "^29.0.2",
"postcss": "^8.5.10",
"prettier": "^3.8.3",
+ "micron-parser": "github:RFnexus/micron-parser-js",
"tailwindcss": "^3.4.19",
"terser": "^5.46.1",
"vitest": "^4.1.4"
@@ -278,7 +279,6 @@
"emoji-picker-element": "^1.29.1",
"emoji-picker-element-data": "^1.8.0",
"marked": "^18.0.0",
- "micron-parser": "github:RFnexus/micron-parser-js",
"mitt": "^3.0.1",
"ol": "^10.9.0",
"protobufjs": "^7.5.5",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 29266e2e..f674eb50 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -61,9 +61,6 @@ importers:
marked:
specifier: ^18.0.0
version: 18.0.0
- micron-parser:
- specifier: github:RFnexus/micron-parser-js
- version: git+https://github.com/RFnexus/micron-parser-js.git#aa8fe3ce9c53fe78c79b39a0abfb3264948b47a9
mitt:
specifier: ^3.0.1
version: 3.0.1
@@ -194,6 +191,9 @@ importers:
jsdom:
specifier: ^29.0.2
version: 29.0.2
+ micron-parser:
+ specifier: github:RFnexus/micron-parser-js
+ version: https://codeload.github.com/RFnexus/micron-parser-js/tar.gz/aa8fe3ce9c53fe78c79b39a0abfb3264948b47a9
postcss:
specifier: ^8.5.10
version: 8.5.10
@@ -407,8 +407,8 @@ packages:
resolution: {integrity: sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==}
engines: {node: '>=14'}
- '@electron/node-gyp@git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2':
- resolution: {commit: 06b29aafb7708acef8b3669835c8a7857ebc92d2, repo: https://github.com/electron/node-gyp.git, type: git}
+ '@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2':
+ resolution: {tarball: https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2}
version: 10.2.0-electron.1
engines: {node: '>=12.13.0'}
hasBin: true
@@ -2622,8 +2622,8 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
- micron-parser@git+https://github.com/RFnexus/micron-parser-js.git#aa8fe3ce9c53fe78c79b39a0abfb3264948b47a9:
- resolution: {commit: aa8fe3ce9c53fe78c79b39a0abfb3264948b47a9, repo: https://github.com/RFnexus/micron-parser-js.git, type: git}
+ micron-parser@https://codeload.github.com/RFnexus/micron-parser-js/tar.gz/aa8fe3ce9c53fe78c79b39a0abfb3264948b47a9:
+ resolution: {tarball: https://codeload.github.com/RFnexus/micron-parser-js/tar.gz/aa8fe3ce9c53fe78c79b39a0abfb3264948b47a9}
version: 0.0.0
mime-db@1.52.0:
@@ -4403,7 +4403,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@electron/node-gyp@git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2':
+ '@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2':
dependencies:
env-paths: 2.2.1
exponential-backoff: 3.1.3
@@ -4466,7 +4466,7 @@ snapshots:
'@electron/rebuild@3.7.2':
dependencies:
- '@electron/node-gyp': git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2
+ '@electron/node-gyp': https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2
'@malept/cross-spawn-promise': 2.0.0
chalk: 4.1.2
debug: 4.4.3
@@ -6959,7 +6959,7 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.2
- micron-parser@git+https://github.com/RFnexus/micron-parser-js.git#aa8fe3ce9c53fe78c79b39a0abfb3264948b47a9: {}
+ micron-parser@https://codeload.github.com/RFnexus/micron-parser-js/tar.gz/aa8fe3ce9c53fe78c79b39a0abfb3264948b47a9: {}
mime-db@1.52.0: {}
diff --git a/scripts/ensure-micron-parser-package.js b/scripts/ensure-micron-parser-package.js
new file mode 100644
index 00000000..31994fbc
--- /dev/null
+++ b/scripts/ensure-micron-parser-package.js
@@ -0,0 +1,26 @@
+#!/usr/bin/env node
+
+const fs = require("fs");
+const path = require("path");
+
+const packageDir = path.join(__dirname, "..", "node_modules", "micron-parser");
+const packageJsonPath = path.join(packageDir, "package.json");
+
+if (!fs.existsSync(packageDir)) {
+ console.log("micron-parser not installed, skipping package metadata normalization.");
+ process.exit(0);
+}
+
+if (fs.existsSync(packageJsonPath)) {
+ process.exit(0);
+}
+
+const packageJson = {
+ name: "micron-parser",
+ version: "0.0.0",
+ main: "js/micron-parser.js",
+ module: "js/micron-parser.js",
+};
+
+fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf8");
+console.log("Created node_modules/micron-parser/package.json for electron-builder dependency scan.");
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────